/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using Xceed.Silverlight.Data;

namespace Xceed.Silverlight.ListBox
{
  internal class SelectionManager
  {
    #region Constructors

    internal SelectionManager( ListBox listBox )
    {
      if( listBox == null )
        throw new ArgumentNullException( "listBox" );

      m_listBox = listBox;
    }

    #endregion

    #region SelectionMode Property

    internal SelectionMode SelectionMode
    {
      get
      {
        return m_selectionMode;
      }
      set
      {
        if( m_selectionMode != value )
        {
          m_selectionMode = value;
          this.OnSelectionModeChanged();
        }
      }
    }

    private SelectionMode m_selectionMode = SelectionMode.Single;

    private void OnSelectionModeChanged()
    {
      using( this.DeferNotifySelectionChanged() )
      {
        this.UnselectAll( true );
      }
    }

    #endregion

    #region StartAnchorPath Property

    private DataPath StartAnchorPath
    {
      get
      {
        return m_startAnchor;
      }
      set
      {
        m_startAnchor = value;
      }
    }

    private DataPath m_startAnchor;

    #endregion

    #region UpdateSelectionOnNextMouseUpElement Property

    private ListBoxContainer UpdateSelectionOnNextMouseUpElement
    {
      get
      {
        return ( m_updateSelectionOnNextMouseUpElement != null ) ? m_updateSelectionOnNextMouseUpElement.Target as ListBoxContainer : null;
      }
      set
      {
        if( ( m_updateSelectionOnNextMouseUpElement == null ) || ( value != m_updateSelectionOnNextMouseUpElement.Target as ListBoxContainer ) )
        {
          if( value == null )
          {
            m_updateSelectionOnNextMouseUpElement = null;
          }
          else
          {
            m_updateSelectionOnNextMouseUpElement = new WeakReference( value );
          }
        }
      }
    }

    private WeakReference m_updateSelectionOnNextMouseUpElement; //null

    #endregion

    #region RangeSelectionRange Property

    private SelectionRange RangeSelectionRange
    {
      get
      {
        return ( m_rangeSelectionRange != null ) ? m_rangeSelectionRange.Target as SelectionRange : null;
      }
      set
      {
        if( ( m_rangeSelectionRange == null ) || ( value != m_rangeSelectionRange.Target as SelectionRange ) )
        {
          if( value == null )
          {
            m_rangeSelectionRange = null;
          }
          else
          {
            m_rangeSelectionRange = new WeakReference( value );
          }
        }
      }
    }

    private WeakReference m_rangeSelectionRange; //null

    #endregion

    #region IsUpdatingSelection Property

    internal bool IsUpdatingSelection
    {
      get
      {
        return ( m_isUpdatingSelection > 0 );
      }
    }

    private int m_isUpdatingSelection;

    private IDisposable DeferNotifySelectionChanged()
    {
      return new DeferNotifySelectionChangedDisposable( this );
    }

    #endregion

    internal event EventHandler SelectionChanged;
    internal event EventHandler UpdateSelectionProperties;

    // This method is use to update the SelectedRanges when SelectedItem is modified directly
    internal void UpdateSingleSelectionHelper( ListBoxContainer container, DataPath dataPath )
    {
      using( this.DeferNotifySelectionChanged() )
      {
        this.SelectJustThisItem( container, dataPath );
      }
    }

    internal void UpdateSelectionHelper( ListBoxContainer container, SelectionAction action, bool isFromAsyncDataSource )
    {
      using( this.DeferNotifySelectionChanged() )
      {
        switch( this.SelectionMode )
        {
          case SelectionMode.Extended:
            this.UpdateExtendedSelection( container, action );
            break;

          case SelectionMode.Multiple:
            this.UpdateMultipleSelection( container, action );
            break;

          case SelectionMode.Single:
            this.UpdateSingleSelection( container, action );
            break;

          default:
            throw new ListBoxInternalException( "Unknown SelectionMode" );
        }
      }
    }

    internal void SelectAllHelper()
    {
     using( this.DeferNotifySelectionChanged() )
      {
        // Not available in Single selection
        if( this.SelectionMode == SelectionMode.Single )
          return;

        // Ensure we do not already have selected all items.
        foreach( SelectionRange selectionRange in m_listBox.SelectedRanges )
        {
          if( selectionRange.SelectFromStart && selectionRange.SelectToEnd )
            return;
        }

        this.UnselectAll( false );

        SelectionRange selectAllRange = SelectionRange.All;

        m_listBox.SelectedRanges.Add( selectAllRange );

        //Raise Internal even to resynchronize all SelectedRanges on layout elements.
        this.RaiseUpdateSelectionProperties();

        //Raise the event to notify user of changes in the Selection.
        this.RaiseSelectionChanged();
      }
    }

    private void UpdateExtendedSelection( ListBoxContainer container, SelectionAction action )
    {
      if( action != SelectionAction.MouseUp )
      {
        m_updateSelectionOnNextMouseUp = false;
        m_updateSelectionOnNextMouseUpElement = null;
      }

      switch( action )
      {
        case SelectionAction.Navigation:
          {
            Key lastNavigationKey = m_listBox.Panel.LastNavigationKey;

            if( ( Keyboard.Modifiers & ModifierKeys.Control ) == ModifierKeys.Control )
            {
              if( ( Keyboard.Modifiers & ModifierKeys.Shift ) == ModifierKeys.Shift )
              {
                this.DoRangeSelection( container );
              }
              else
              {
                this.StartAnchorPath = ( container != null ) ? container.DataPath : null;

                if( ( lastNavigationKey == Key.PageUp )
                  || ( lastNavigationKey == Key.PageDown )
                  || ( lastNavigationKey == Key.End )
                  || ( lastNavigationKey == Key.Home ) )
                {
                  if( ( container == null ) || !container.IsSelectable )
                  {
                    this.UnselectAll();

                    this.RangeSelectionRange = null;
                  }
                  else
                  {
                    this.SelectJustThisItem( container, container.DataPath );

                    //Store the selection range in the SelectionManager since it is possible after this single select, the user
                    //will try to perform range selection.
                    this.RangeSelectionRange = container.SelectionRange;
                  }
                }
              }
            }
            else if( ( Keyboard.Modifiers & ModifierKeys.Shift ) == ModifierKeys.Shift )
            {
              // We need to unselect every previous selection before processing
              // since Ctrl key is not down
              this.UnselectAll();

              this.DoRangeSelection( container );
            }
            else
            {
              this.StartAnchorPath = ( container != null ) ? container.DataPath : null;

              if( ( container == null ) || !container.IsSelectable )
              {
                this.UnselectAll();

                this.RangeSelectionRange = null;
              }
              else
              {
                this.SelectJustThisItem( container, container.DataPath );

                //Store the selection range in the SelectionManager since it is possible after this single select, the user
                //will try to perform range selection.
                this.RangeSelectionRange = container.SelectionRange;
              }
            }

            break;
          }

        case SelectionAction.MouseDown:
          {
            if( ( Keyboard.Modifiers & ModifierKeys.Control ) == ModifierKeys.Control )
            {
              if( ( Keyboard.Modifiers & ModifierKeys.Shift ) == ModifierKeys.Shift )
              {
                this.DoRangeSelection( container );
                // Do not update the StartAnchor with the last container DataPath
                // This is the same behavior as MS ListBox
              }
              else
              {
                this.StartAnchorPath = ( container != null ) ? container.DataPath : null;

                //If the row is already selected, I want to update selection status only on the MouseUp, this is to allow Drag/Drop opeartions.
                //If the row is not already selected, we update selection on MouseDown to allow Drag/Drop operations.
                // this.Select return false when the item is already selected.
                if( ( container != null ) && container.IsSelectable )
                {
                  if( container.IsSelected )
                  {
                    m_updateSelectionOnNextMouseUp = true;
                    this.UpdateSelectionOnNextMouseUpElement = container;
                  }
                  else
                  {
                    this.SelectJustThisItem( container, container.DataPath, false );

                    //Store the selection range in the SelectionManager since it is possible after this single select, the user
                    //will try to perform range selection.
                    this.RangeSelectionRange = container.SelectionRange;
                  }
                }
              }
            }
            else if( ( Keyboard.Modifiers & ModifierKeys.Shift ) == ModifierKeys.Shift )
            {
              // We need to unselect every previous selection before processing
              // since Ctrl key is not down.
              // Do not notify for this UnSelectAll since a
              // SelectionChanged will be raised by DoRangeSelection
              this.UnselectAll( false );

              this.DoRangeSelection( container );
            }
            else
            {
              this.StartAnchorPath = ( container != null ) ? container.DataPath : null;

              if( ( container == null ) || !container.IsSelectable )
              {
                this.UnselectAll();

                this.RangeSelectionRange = null;
              }
              else if( container.IsSelected )
              {
                m_updateSelectionOnNextMouseUp = true;
                this.UpdateSelectionOnNextMouseUpElement = container;
              }
              else
              {
                this.SelectJustThisItem( container, container.DataPath, true );

                //Store the selection range in the SelectionManager since it is possible after this single select, the user
                //will try to perform range selection.
                this.RangeSelectionRange = container.SelectionRange;
              }
            }

            break;
          }

        case SelectionAction.MouseUp:
          {
            //Only process MouseUp if the same ListBoxContainer has MouseUp
            bool isMouseUpMatch = m_updateSelectionOnNextMouseUp && ( container == this.UpdateSelectionOnNextMouseUpElement );

            if( isMouseUpMatch )
            {
              if( ( container != null ) && container.IsSelectable )
              {
                if( ( Keyboard.Modifiers & ModifierKeys.Control ) == ModifierKeys.Control )
                {
                  this.ToggleItemSelection( container );

                  //TODO: What to do for startAnchor and RangeSelectionRange?
                }
                else
                {
                  this.SelectJustThisItem( container, container.DataPath );

                  //Store the selection range in the SelectionManager since it is possible after this single select, the user
                  //will try to perform range selection.
                  this.RangeSelectionRange = container.SelectionRange;
                }
              }
            }

            // We reset the m_updateSelectionOnNextMouseUp to be sure when no focus is moved 
            // around that mouse up will toggle selection
            m_updateSelectionOnNextMouseUp = false;
            m_updateSelectionOnNextMouseUpElement = null;
            break;
          }

        case SelectionAction.Keyboard:
          {
            this.UpdateSelectionOnNextMouseUpElement = container;


            if( ( container != null ) && container.IsSelectable )
            {
              this.ToggleItemSelection( container );
              this.StartAnchorPath = ( container != null ) ? container.DataPath : null;
              this.RangeSelectionRange = container.SelectionRange;
            }

            break;
          }
      }
    }

    private void UpdateMultipleSelection( ListBoxContainer container, SelectionAction action )
    {
      DataPath dataPath = ( container != null ) ? container.DataPath : null;
      DataItem dataItem = ( dataPath != null ) ? dataPath.LastChild : null;

      //Multiple Selection mode does not support key modifiers for range selection such as CTRL or SHIFT

      if( action != SelectionAction.MouseUp )
      {
        m_updateSelectionOnNextMouseUp = false;
        m_updateSelectionOnNextMouseUpElement = null;
      }

      switch( action )
      {
        case SelectionAction.MouseDown:
          {

            if( ( container != null ) && ( container.IsSelectable ) )
            {
              //If the row is already selected, I want to update selection status only on the MouseUp, this is to allow Drag/Drop opeartions.
              //If the row is not already selected, we update selection on MouseDown to allow Drag/Drop operations.
              // this.Select return false when the item is already selected.

              if( container.IsSelected )
              {
                m_updateSelectionOnNextMouseUp = true;
                this.UpdateSelectionOnNextMouseUpElement = container;
              }
              else
              {
                this.SelectJustThisItem( container, container.DataPath, false );
              }
            }

            break;
          }

        case SelectionAction.MouseUp:
          {
            //Only process MouseUp if the same ListBoxContainer has MouseUp
            bool isMouseUpMatch = m_updateSelectionOnNextMouseUp && ( container == this.UpdateSelectionOnNextMouseUpElement );

            if( isMouseUpMatch )
            {
              if( ( container != null ) && ( container.IsSelectable ) ) //Condition is placed there, but is redundant with the one in the MouseDown handling.
              {
                this.ToggleItemSelection( container );
              }
            }

            m_updateSelectionOnNextMouseUp = false;
            m_updateSelectionOnNextMouseUpElement = null;
            break;
          }

        case SelectionAction.Keyboard:
          {
            if( ( container != null ) && ( container.IsSelectable ) )
            {
              this.ToggleItemSelection( container );
            }
            break;
          }
      }
    }

    private void UpdateSingleSelection( ListBoxContainer container, SelectionAction action )
    {
      if( action != SelectionAction.MouseUp )
      {
        m_updateSelectionOnNextMouseUp = false;
        m_updateSelectionOnNextMouseUpElement = null;
      }

      bool doSelection = false;

      bool isCtrlDown = ( Keyboard.Modifiers & ModifierKeys.Control ) == ModifierKeys.Control;

      //Keyboard Nav
      if( action == SelectionAction.Navigation )
      {
        if( !isCtrlDown )
        {
          //If Control Key is held, that means we shall move currency without moving selection.
          //Absence of CTRL modifier means we move selection, hence doSelection = true.
          doSelection = true;
        }
        else //this means that CTRL was pressed
        {
          Key lastNavigationKey = m_listBox.Panel.LastNavigationKey;

          //CTRL-Home, CTRL-End, CTRL-PageUp and CTRL-PageDown are specific navigation shortcuts for which the above mentionned limitation
          //does not apply, hence setting the doSelection at True.
          if( ( lastNavigationKey == Key.PageUp )
            || ( lastNavigationKey == Key.PageDown )
            || ( lastNavigationKey == Key.End )
            || ( lastNavigationKey == Key.Home ) )
          {
            doSelection = true;
          }
        }
      }
      else if( action == SelectionAction.MouseUp )
      {
        // The selection is updated in MouseDown for SelectionMode.Single
        return;
      }
      else //Mouse, Direct Keyboard Selection (Space), or ...
      {
        doSelection = true;
      }

      if( !doSelection )
        return;

      if( ( container != null ) && ( container.IsSelectable ) )
      {
        // If Ctrl is down, we want to toggle the selection, else we just select this
        // item if it is not already selected
        if( isCtrlDown )
        {
          if( !container.IsSelected )
          {
            this.SelectJustThisItem( container, container.DataPath, true );
          }
          else
          {
            this.UnselectAll();
          }
        }
        else
        {
          this.SelectJustThisItem( container, container.DataPath );
        }
      }
      else
      {
        this.UnselectAll();
      }
    }

    internal void UnselectAllHelper()
    {
      using( this.DeferNotifySelectionChanged() )
      {
        this.UnselectAll( true );
      }
    }

    private void UnselectAll()
    {
      this.UnselectAll( true );
    }

    private void UnselectAll( bool notify )
    {
      bool changesDone = false;

      if( m_listBox.SelectedRanges.Count > 0 )
      {
        m_listBox.SelectedRanges.Clear();
        changesDone = true;
      }

      if( changesDone )
      {
        this.RaiseUpdateSelectionProperties();
      }

      // Only raise SelectionChanged if changes were done
      if( notify && changesDone )
      {
        this.RaiseSelectionChanged();
      }
    }

    internal void NotifySelectionChanged()
    {
      using( this.DeferNotifySelectionChanged() )
      {
        //Raise Internal event to resynchronize all SelectedRanges on layout elements.
        this.RaiseUpdateSelectionProperties();

        //Raise the event to notify user of changes in the Selection.
        this.RaiseSelectionChanged();

      }
    }

    private void SelectJustThisItem( ListBoxContainer container, DataPath dataPath )
    {
      this.SelectJustThisItem( container, dataPath, true );
    }

    private void SelectJustThisItem( ListBoxContainer container, DataPath dataPath, bool unselect )
    {
      if( unselect )
      {
        this.UnselectAll( false );
      }

      SelectionRange newRange = SelectionManager.SelectionRangeHelper( m_listBox.DataSourceWrapper,
        dataPath,
        true,
        dataPath,
        true );

      m_listBox.SelectedRanges.Add( newRange );

      if( container != null )
      {
        container.SelectionRange = newRange;
        container.IsSelected = true;
      }

      this.RaiseSelectionChanged();
    }

    public void ToggleItemSelection( ListBoxContainer container )
    {
      //If the element is currently selected
      if( container.IsSelected )
      {
        //retrieve current SelectionRange for item (can only belong to one at a time).
        SelectionRange currentRange = container.SelectionRange;

        Debug.Assert( currentRange != null, "A selected container without any SelectionRange..." );

        //retrieve current SelectionRange for item (can only belong to one at a time).
        m_listBox.SelectedRanges.Remove( currentRange );

        //If the selection range only represents this item ( start of range equals end of range ), then there is nothing additional to do.
        //If the range possibly represents more than one item, in that case, we need to split the range and update all containers.
        if( !currentRange.IsSingleItem )
        {
          DataPath dataPath = container.DataPath;

          //Compute the split
          IEnumerable<SelectionRange> newRanges = SelectionManager.SplitSelectedRange( m_listBox.DataSourceWrapper,
            currentRange,
            dataPath,
            dataPath );

          //iterate through split ranges
          foreach( SelectionRange newRange in newRanges )
          {
            m_listBox.SelectedRanges.Add( newRange );
          }

          //Raise Internal even to resynchronize all SelectedRanges on layout elements.
          this.RaiseUpdateSelectionProperties();
        }
        else
        {
          // Ensure to set the element as IsSelected = false directly on the element
          // instead of calling the RaiseUpdateSelectionProperties when only
          // one item is affected by this selection change
          container.SelectionRange = null;
          container.IsSelected = false;
        }
      }
      else
      {
        //Element is not flagged as already selected, we just need to create a new range for item and "select" it.
        DataPath dataPath = container.DataPath;

        SelectionRange newRange = SelectionManager.SelectionRangeHelper( m_listBox.DataSourceWrapper, dataPath, true, dataPath, true );

        m_listBox.SelectedRanges.Add( newRange );

        container.SelectionRange = newRange;
        container.IsSelected = true;
      }

      //Raise the public event for the user of the component.
      this.RaiseSelectionChanged();
    }

    private void DoRangeSelection( ListBoxContainer container )
    {
      if( container == null )
        throw new ArgumentNullException( "container cannot be null." );

      if( !container.IsSelectable )
        return;

      DataPath startPath = this.StartAnchorPath;

      //No start anchor defined, define it and "single select" the element passed.
      if( startPath == null )
      {
        this.UpdateStartAnchorPath( container );
        this.SelectJustThisItem( container, container.DataPath, false );
        return;
      }

      DataPath endPath = container.DataPath;
      SelectionRange selectionRange = this.RangeSelectionRange;

      if( ( selectionRange != null ) && ( this.CompareDataItems( selectionRange.SortDescriptions, startPath.LastChild, endPath.LastChild ) < 0 ) )
      {
        //This means the end is "smaller" than the start. To have consistent Data, it is better than the End is always after the Start.
        DataPath swap = endPath;
        endPath = startPath;
        startPath = swap;
      }

      m_listBox.SelectedRanges.Remove( selectionRange );

      // Create a new SelectionRange with the new start and end DataPath
      // to be able to compare if the end is "smalles" than the start correctly
      selectionRange = SelectionManager.SelectionRangeHelper( m_listBox.DataSourceWrapper, startPath, true, endPath, true );
      this.RangeSelectionRange = selectionRange;
      m_listBox.SelectedRanges.Add( selectionRange );

      //Raise Internal even to resynchronize all SelectedRanges on layout elements.
      this.RaiseUpdateSelectionProperties();

      //Raise the event to notify user of changes in the Selection.
      this.RaiseSelectionChanged();
    }

    private void UpdateStartAnchorPath( ListBoxContainer container )
    {
      if( ( container != null ) && ( container.IsSelectable ) )
      {
        this.StartAnchorPath = container.DataPath;
      }
      else
      {
        this.StartAnchorPath = null;
      }
    }

    internal static IEnumerable<SelectionRange> SplitSelectedRange(
      DataSourceWrapper dataSource,
      SelectionRange currentRange,
      DataPath splitStart,
      DataPath splitEnd )
    {
      //By definition, a split can separate a given SelectionRange in 1 (if start or end are at one extremity) or 2 ( if start and end are not at extremities)
      List<SelectionRange> returnList = new List<SelectionRange>();

      if( currentRange.SelectFromStart )
      {
        //create the new Range from the old range start to the split start.
        SelectionRange startRange = SelectionManager.SelectionRangeHelper( dataSource,
          currentRange.SortDescriptions,
          currentRange.StartRangeInfos,
          currentRange.StartRangeInfos.IsInclusive,
          splitStart,
          false,
          true );

        returnList.Add( startRange );
      }
      else
      {
        int splitStartComparison = SelectionManager.CompareSelectionRangeStart( dataSource,
          currentRange,
          splitStart );

        if( splitStartComparison < 0 )
          throw new ListBoxInternalException(); //Something was very wrong with values passed to the Split method.

        // > 0 means that the splitStart is after the currentRange, meaning I actually have to split the range (not only truncate)
        if( splitStartComparison > 0 )
        {
          //create the new Range from the old range start to the split start.
          SelectionRange startRange = SelectionManager.SelectionRangeHelper( dataSource,
            currentRange.SortDescriptions,
            currentRange.StartRangeInfos,
            currentRange.StartRangeInfos.IsInclusive,
            splitStart,
            false,
            false );

          returnList.Add( startRange );
        }
      }

      if( currentRange.SelectToEnd )
      {
        SelectionRange endRange = SelectionManager.SelectionRangeHelper( dataSource,
            currentRange.SortDescriptions,
            splitEnd,
            false,
            currentRange.EndRangeInfos,
            currentRange.EndRangeInfos.IsInclusive,
            true );

        returnList.Add( endRange );
      }
      else
      {
        int splitEndComparison = SelectionManager.CompareSelectionRangeEnd( dataSource,
          currentRange,
          splitEnd );

        if( splitEndComparison > 0 )
          throw new ListBoxInternalException(); //Something was very wrong with values passed to the Split method.

        // < 0 means that the splitEnd is before the end of the currentRange, meaning I actually have to split the range (not only truncate)
        if( splitEndComparison < 0 )
        {
          SelectionRange endRange = SelectionManager.SelectionRangeHelper( dataSource,
            currentRange.SortDescriptions,
            splitEnd,
            false,
            currentRange.EndRangeInfos,
            currentRange.EndRangeInfos.IsInclusive,
            false );

          returnList.Add( endRange );
        }
      }

      Debug.Assert( returnList.Count != 0 );

      return returnList;
    }

    internal static SelectionRange SelectionRangeHelper( DataSourceWrapper dataSourceWrapper,
      DataPath rangeStart,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive )
    {
      if( rangeStart.Count() != rangeEnd.Count() )
        throw new InvalidOperationException( "rangeStart is not consistent with rangeEnd. DataPaths do not have same size." );

      DataItem rangeStartDataItem = rangeStart.LastChild;
      DataItem rangeEndDataItem = rangeEnd.LastChild;

      if( rangeStartDataItem == null )
        throw new ArgumentException( "rangeStart contains invalid data. ", "rangeStart" );

      if( rangeEndDataItem == null )
        throw new ArgumentException( "rangeEnd contains invalid data. ", "rangeEnd" );

      Type rangeStartType = rangeStartDataItem.GetType();

      if( !rangeStartType.Equals( rangeEndDataItem.GetType() ) )
        throw new ArgumentException( "rangeEnd's object does not have the same Type as rangeStart's object." );

      SelectionRange returnValue = new SelectionRange();

      foreach( SortDescription sortDesc in dataSourceWrapper.AppliedSortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        returnValue.StartRangeInfos.Add( propName, dataSourceWrapper.GetDataObjectPropertyValue( propName, rangeStartDataItem ) );
        returnValue.EndRangeInfos.Add( propName, dataSourceWrapper.GetDataObjectPropertyValue( propName, rangeEndDataItem ) );
      }

      returnValue.StartRangeInfos.IsInclusive = startInclusive;
      returnValue.EndRangeInfos.IsInclusive = endInclusive;

      return returnValue;
    }

    private static SelectionRange SelectionRangeHelper(
      DataSourceWrapper dataSourceWrapper,
      IList<SortDescription> sortDescriptions,
      IDictionary<string, object> startValues,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      bool fromStart )
    {
      DataItem rangeEndDataItem = rangeEnd.LastChild;

      if( rangeEndDataItem == null )
        throw new ArgumentException( "rangeEnd contains invalid data. ", "rangeEnd" );

      bool startValuesReachedEnd = false;

      Type rangeEndType = rangeEndDataItem.GetType();

      SelectionRange returnValue = new SelectionRange();

      if( fromStart )
        returnValue.StartRangeInfos.FromStart();

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        if( !fromStart )
        {
          object startValue = null;

          //Try to retrieve data from the startValues parameter
          if( !startValues.TryGetValue( propName, out startValue ) )
          {
            //If data is not found, flag the end of data for the startValues
            startValuesReachedEnd = true;
          }
          else
          {
            //This particular condition could occur if a property is missing in the startValue, but in the middle of the SortDescriptions range. 
            // While it is supported that the startValues does not have the same level of completeness as the endValues, it shall be by being shorter at the end,
            //not in the middle
            if( startValuesReachedEnd )
              throw new ListBoxInternalException();

            returnValue.StartRangeInfos.Add( propName, startValue );
          }
        }

        returnValue.EndRangeInfos.Add( propName, dataSourceWrapper.GetDataObjectPropertyValue( propName, rangeEndDataItem ) );
      }

      returnValue.StartRangeInfos.IsInclusive = startInclusive;
      returnValue.EndRangeInfos.IsInclusive = endInclusive;

      return returnValue;
    }

    private static SelectionRange SelectionRangeHelper(
      DataSourceWrapper dataSourceWrapper,
      IList<SortDescription> sortDescriptions,
      DataPath rangeStart,
      bool startInclusive,
      IDictionary<string, object> endValues,
      bool endInclusive,
      bool untilEnd )
    {
      DataItem rangeStartDataItem = rangeStart.LastChild;

      if( rangeStartDataItem == null )
        throw new ArgumentException( "rangeStart contains invalid data. ", "rangeStart" );

      bool endValuesReachedEnd = false;

      Type rangeStartType = rangeStartDataItem.GetType();

      SelectionRange returnValue = new SelectionRange();

      if( untilEnd )
        returnValue.EndRangeInfos.ToEnd();

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        if( !untilEnd )
        {
          object endValue = null;

          //Try to retrieve data from the startValues parameter
          if( !endValues.TryGetValue( propName, out endValue ) )
          {
            //If data is not found, flag the end of data for the startValues
            endValuesReachedEnd = true;
          }
          else
          {
            //This particular condition could occur if a property is missing in the startValue, but in the middle of the SortDescriptions range. 
            // While it is supported that the startValues does not have the same level of completeness as the endValues, it shall be by being shorter at the end,
            //not in the middle
            if( endValuesReachedEnd )
              throw new ListBoxInternalException();

            returnValue.EndRangeInfos.Add( propName, endValue );
          }
        }

        returnValue.StartRangeInfos.Add( propName, dataSourceWrapper.GetDataObjectPropertyValue( propName, rangeStartDataItem ) );
      }

      returnValue.StartRangeInfos.IsInclusive = startInclusive;
      returnValue.EndRangeInfos.IsInclusive = endInclusive;

      return returnValue;
    }

    internal static int CompareSelectionRangeStart(
      DataSourceWrapper dataSource,
      SelectionRange currentRange,
      DataPath referencePath )
    {
      return SelectionManager.CompareSelectionRangeHelper( dataSource,
        currentRange.SortDescriptions,
        currentRange.StartRangeInfos,
        referencePath.LastChild );
    }

    internal static int CompareSelectionRangeEnd(
      DataSourceWrapper dataSource,
      SelectionRange currentRange,
      DataPath referencePath )
    {
      return SelectionManager.CompareSelectionRangeHelper( dataSource,
        currentRange.SortDescriptions,
        currentRange.EndRangeInfos,
        referencePath.LastChild );
    }

    private static int CompareSelectionRangeHelper( DataSourceWrapper dataSource,
      IList<SortDescription> sortDescriptions,
      IDictionary<String, object> keyValues,
      DataItem referenceDataItem )
    {
      if( sortDescriptions == null )
        throw new ArgumentNullException( "sortDescriptions" );

      if( keyValues == null )
        throw new ArgumentNullException( "keyValues" );

      if( referenceDataItem == null )
        throw new ArgumentNullException( "referenceDataItem" );

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        object keyValue;

        if( !keyValues.TryGetValue( propName, out keyValue ) )
        {
          keyValue = null;
        }

        object referenceValue = dataSource.GetDataObjectPropertyValue( propName, referenceDataItem );
        int result = SelectionManager.CompareHelper( referenceValue, keyValue );

        if( result < 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return -1;
          }
          else
          {
            return 1;
          }
        }

        if( result > 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return 1;
          }
          else
          {
            return -1;
          }
        }
      }

      return 0;
    }

    private static int CompareHelper( object a, object b )
    {
      if( a == b )
        return 0;

      if( a == null )
        return -1;

      if( b == null )
        return 1;

      IComparable comparable = a as IComparable;

      if( comparable == null )
        throw new ArgumentException( "Object must Implement IComparable.", "a" );

      return comparable.CompareTo( b );
    }

    private int CompareDataItems( IList<SortDescription> sortDescriptions, DataItem currentItem, DataItem referenceItem )
    {
      if( sortDescriptions == null )
        throw new ArgumentNullException( "sortDescriptions" );

      if( currentItem == null )
        throw new ArgumentNullException( "currentItem" );

      if( referenceItem == null )
        throw new ArgumentNullException( "referenceItem" );

      if( currentItem.Data == null )
        throw new ArgumentException( "The specified DataItem does not contain data.", "currentItem" );

      if( referenceItem.Data == null )
        throw new ArgumentException( "The specified DataItem does not contain data.", "referenceItem" );

      DataSourceWrapper dataSource = m_listBox.DataSourceWrapper;

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        object currentValue = dataSource.GetDataObjectPropertyValue( propName, currentItem );
        object referenceValue = dataSource.GetDataObjectPropertyValue( propName, referenceItem );

        int result = SelectionManager.CompareHelper( referenceValue, currentValue );

        if( result < 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return -1;
          }
          else
          {
            return 1;
          }
        }

        if( result > 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return 1;
          }
          else
          {
            return -1;
          }
        }
      }

      return 0;
    }

    private void RaiseSelectionChanged()
    {
      if( this.SelectionChanged != null )
      {
        this.SelectionChanged( this, EventArgs.Empty );
      }
    }

    private void RaiseUpdateSelectionProperties()
    {
      if( this.UpdateSelectionProperties != null )
      {
        this.UpdateSelectionProperties( this, EventArgs.Empty );
      }
    }

    private bool m_updateSelectionOnNextMouseUp; //false
    private ListBox m_listBox; //null

    #region DeferNotifySelectionChangedDisposable private class

    private class DeferNotifySelectionChangedDisposable : IDisposable
    {
      public DeferNotifySelectionChangedDisposable( SelectionManager manager )
      {
        if( manager == null )
          throw new ArgumentNullException( "manager" );

        m_manager = manager;
        m_manager.m_isUpdatingSelection++;
      }

      public void Dispose()
      {
        if( m_manager == null )
          return;

        m_manager.m_isUpdatingSelection--;
        m_manager = null;
      }

      private SelectionManager m_manager;
    }

    #endregion
  }
}
